MVVM in Action Advanced MVVM Design for Real World Applications
Olivier Driesbach, Engineer, Cross
April 19, 2016
ZK Studio 2.0.1
Introduction
Since version 6 of ZK, developers can choose to create web applications using the MVVM design pattern as an alternative to the traditional MVC one. If you're not familiar with MVVM, I recommend reading through the following articles as a good starting point:
Purpose of This Article
As explained in the ZK Advanced section (Chapter 4.4: Wire Components) of the online documentation, it is not suggested to wire UI components into your ViewModels because you will loose the main advantage of using the MVVM design pattern: separating view's concerns from ViewModel operations.
Unfortunately, if you've experienced the development of a real world ZK application, you've probably discovered how difficult it is to fully respect this condition, especially when you need to open/close a new window at the end of an action.
This article will show you how this is possible with some java templates using ZK MVVM basic concepts.
Context
The full source code of this article is based on a real world application use case. It has been adapted to its minimalistic version in order to be reusable like a conception pattern. It is available as a download at the end of the article.
- Use case
- The user goes to the list's page of persons.
- The user selects one person.
- The user clicks on the "Address" button to open a new window.
- The new window displays the list of adresses available in the system.
- The user selects one address in the list and click on the "Select" button.
- The selected adress is attached to the selected person of the first window and the address window is closed automatically.
- Known difficulties
- Create the address button on the person's page without coding the URI of the addresses' page into the button's action of the ViewModel.
- Send the selected address (POJO or identifier) from the address ViewModel to the person ViewModel without having references between each other (separation of concerns).
- Close the addresses' window without coding the UI stuff in the ViewModel.
- Person and Address (Model, View and ViewModel)
The Person.java (Model)
package demo.mvvm.person;
import demo.mvvm.address.Address;
public class Person {
private String firstName;
private String lastName;
private String email;
private Address address;
public Person() {
}
public Person(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
// Getters and setters ...
}
The Address.java (Model)
package demo.mvvm.address;
public class Address {
private String number;
private String line1;
private String line2;
private String zipCode;
private String city;
public Address() {
}
public Address(String number, String line1, String line2, String zipCode, String city) {
this.number = number;
this.line1 = line1;
this.line2 = line2;
this.zipCode = zipCode;
this.city = city;
}
// Getters and setters ...
public String getCompleteAddress() {
return getNumber() +" "+ getLine1() +" "+ getZipCode() +" "+ getCity();
}
}
The personList.zul (View)
<window id="mainPanel">
<vlayout viewModel="@id('personVM') @init('demo.mvvm.person.PersonViewModel')">
<listbox model="@load(personVM.persons)" selectedItem="@bind(personVM.selectedPerson)">
<listhead>
<listheader label="Last name" width="100px" />
<listheader label="First name" width="100px" />
<listheader label="Email" />
<listheader label="Address" />
</listhead>
<template name="model">
<listitem>
<listcell label="@load(each.lastName)" />
<listcell label="@load(each.firstName)" />
<listcell label="@load(each.email)" />
<listcell label="@load(each.address.completeAddress)" />
</listitem>
</template>
</listbox>
<button label="Address" disabled="@load(empty personVM.selectedPerson)" />
</vlayout>
</window>
The addressList.zul (View)
<window title="Addresses" mode="modal" width="800px" closable="true">
<vlayout viewModel="@id('addressVM') @init('demo.mvvm.address.AddressViewModel')">
<listbox model="@load(addressVM.addresses)" selectedItem="@bind(addressVM.selectedAddress)">
<listhead>
<listheader label="Number" width="100px" />
<listheader label="Line 1" />
<listheader label="Line 2" />
<listheader label="Zip Code" width="100px" />
<listheader label="City" width="100px" />
</listhead>
<template name="model">
<listitem>
<listcell label="@load(each.number)" />
<listcell label="@load(each.line1)" />
<listcell label="@load(each.line2)" />
<listcell label="@load(each.zipCode)" />
<listcell label="@load(each.city)" />
</listitem>
</template>
</listbox>
<button label="Validate" disabled="@load(empty addressVM.selectedAddress)" />
</vlayout>
</window>
The PersonViewModel.java
package demo.mvvm.person;
import java.util.List;
import org.zkoss.bind.annotation.Init;
import org.zkoss.bind.annotation.NotifyChange;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import demo.mvvm.address.Address;
public class PersonViewModel {
protected List<Person> persons;
private Person selectedPerson;
public void initPersonViewModel() {
initPersons();
}
public List<Person> getPersons() {
return persons;
}
public Person getSelectedPerson() {
return selectedPerson;
}
public void setSelectedPerson(Person selectedPerson) {
this.selectedPerson = selectedPerson;
}
protected void initPersons() {
persons = new PersonService().findAll();
}
}
The AddressViewModel.java
package demo.mvvm.address;
import java.util.List;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.Init;
public class AddressViewModel {
private List<Address> addresses;
private Address selectedAddress;
public void initAddressViewModel() {
initAddresses();
}
public List<Address> getAddresses() {
return addresses;
}
public Address getSelectedAddress() {
return selectedAddress;
}
public void setSelectedAddress(Address selectedAddress) {
this.selectedAddress = selectedAddress;
}
private void initAddresses() {
addresses = new AddressService().findAll();
}
}
The PersonService.java
package demo.mvvm.person;
import java.util.ArrayList;
import java.util.List;
import demo.mvvm.Person;
public class PersonService {
public PersonService() {}
public List<Person> findAll() {
ArrayList<Person> result = new ArrayList<Person>();
result.add(new Person("John", "Doe", "john.doe@unknown.com"));
result.add(new Person("Oswald", "Cobblepot", "pinguin@linux.com"));
result.add(new Person("Marcus", "Brody", "mbrody@indiana.com"));
result.add(new Person("Thomas", "Anderson", "neo@matrix.com"));
return result;
}
}
And the AddressService.java
package demo.mvvm.address;
import java.util.ArrayList;
import java.util.List;
import demo.mvvm.Address;
public class AddressService {
public AddressService() {
}
public List<Address> findAll() {
ArrayList<Address> result = new ArrayList<Address>();
result.add(new Address("21", "Jump Street", "", "10001", "New York City"));
result.add(new Address("23", "Sec. 1", "Changan E. Rd #7F-2", "10441", "Taipei City"));
result.add(new Address("36", "Quai des Orfèvres", "", "75000", "Paris"));
result.add(new Address("1600", "Pennsylvania Avenue NW", "", "DC 20500", "Washington"));
return result;
}
}
Step by Step Implementation
- Create dynamic interactions with the right conditions
- Communication between ViewModels
When you design the ZUML structure of your "single one page" application, you need to take care of the design of your ZUL pages if you don't want to have huge files with many components and ViewModel instances loaded in-memory when the end-user interface is not displaying them.
Ideally, in our case, if the end user doesn't click on the "Address" button, we don't want to have the "addressList.zul" file integrated in the ZUML structure because the ViewModel will be created too.
A bad integration would look like
<window id="mainPanel">
<div visible="@load(not empty personVM.selectedPerson)">
<include src="otherPage.zul" />
</div>
</window>
Depending on the root component of the otherPage.zul (a modal window for example), this code will not work and you would have to put the visible attribute condition on the window element.
To prevent creating these kinds of visibility dependencies between components, you need to use the include tag. In the CE and EE version of ZK, you can use the apply alternative which is very similar.
<window id="mainPanel">
<vlayout viewModel="@id('personVM') @init('demo.mvvm.person.PersonViewModel')">
<listbox model="@load(personVM.persons)" selectedItem="@bind(personVM.selectedPerson)">
...
</listbox>
<button label="Address" disabled="@load(empty personVM.selectedPerson)" onClick="@command('selectAddress')" />
<include src="@load(personVM.selectAddress ? 'addressList.zul' : '')" />
</vlayout>
</window>
Now, the "selectAddress" command, will enable/disable a boolean property which is binded in the src attribute of the include tag. The tip is using the empty value to attach/detach the page to include only when necessary. Therefore, the included ViewModel will be instanciated dynamically.
package demo.mvvm.person;
import java.util.List;
import org.zkoss.bind.annotation.BindingParam;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.GlobalCommand;
import org.zkoss.bind.annotation.Init;
import org.zkoss.bind.annotation.NotifyChange;
import demo.mvvm.address.Address;
public class PersonViewModel {
protected List<Person> persons;
private Person selectedPerson;
private boolean selectAddress;
@Init(superclass=true)
public void initPersonViewModel() {
initPersons();
selectAddress = false;
}
// Getters and setters ...
public boolean isSelectAddress() {
return selectAddress;
}
public void setSelectAddress(boolean selectAddress) {
this.selectAddress = selectAddress;
}
@Command
@NotifyChange("selectAddress")
public void selectAddress() {
selectAddress = true; // Activate the address selection mode
}
protected void initPersons() {
persons = new PersonService().findAll();
}
}
The recommended way to establish communication between ViewModels is to use the @GlobalCommand mecanism. Regarding our use case, we need to implement the closing of the address window when the user click on the validate button and the default close button of the window component.
Update the Address button
<window title="Addresses" mode="modal" width="800px" closable="true" onClose="@global-command('cancelAddress')">
<vlayout viewModel="@id('addressVM') @init('demo.mvvm.address.AddressViewModel')">
<listbox model="@load(addressVM.addresses)" selectedItem="@bind(addressVM.selectedAddress)">
...
</listbox>
<button label="Validate" disabled="@load(empty addressVM.selectedAddress)"
onClick="@global-command('updateAddress', address=addressVM.selectedAddress)" />
</vlayout>
</window>
Below the PersonViewModel updated accordingly
package demo.mvvm.person;
import java.util.List;
import org.zkoss.bind.annotation.BindingParam;
import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.GlobalCommand;
import org.zkoss.bind.annotation.Init;
import org.zkoss.bind.annotation.NotifyChange;
import demo.mvvm.address.Address;
public class PersonViewModel {
protected List<Person> persons;
private Person selectedPerson;
private boolean selectAddress;
@Init(superclass=true)
public void initPersonViewModel() {
initPersons();
selectAddress = false;
}
// Getters and setters ...
@Command
@NotifyChange("selectAddress")
public void selectAddress() {
selectAddress = true; // Activate the address selection mode
}
@GlobalCommand
@NotifyChange({"selectedPerson", "selectAddress"})
public void updateAddress(@BindingParam("address") Address address) {
getSelectedPerson().setAddress(address);
selectAddress = false; // Disable the address selection mode
}
@GlobalCommand
@NotifyChange("selectAddress")
public void cancelAddress() {
selectAddress = false; // Disable the address selection mode
}
protected void initPersons() {
persons = new PersonService().findAll();
}
}
Source code
Click here for the full source code
This article is contributed by Olivier Driesbach for the community. Feel free to leave him a comment below or create a discussion on ZK forum.
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |